////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2008 T. Scot Clausing
//	http://blog.tsclausing.com
//	http://deploydaily.com
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
////////////////////////////////////////////////////////////////////////////////
package com.deploydaily.mysimplemp3
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.SecurityErrorEvent;
	import flash.events.TimerEvent;
	import flash.media.ID3Info;
	import flash.media.Sound;
	import flash.media.SoundChannel;
	import flash.media.SoundLoaderContext;
	import flash.media.SoundTransform;
	import flash.net.URLRequest;
	import flash.utils.Timer;
	
	//--------------------
	// EVENTS
	//--------------------
	[Event(name="loadingProgressEvent", type="com.deploydaily.mysimplemp3.SimpleMP3ProgressEvent")]
	[Event(name="playbackProgressEvent", type="com.deploydaily.mysimplemp3.SimpleMP3ProgressEvent")]
	
	[Event(name="loadingBegin", type="flash.events.Event")]
	[Event(name="loadingId3Complete", type="flash.events.Event")]
	[Event(name="loadingComplete", type="flash.events.Event")]
	[Event(name="playbackBegin", type="flash.events.Event")]
	[Event(name="playbackPlay", type="flash.events.Event")]
	[Event(name="playbackPause", type="flash.events.Event")]
	[Event(name="playbackResume", type="flash.events.Event")]
	[Event(name="playbackStop", type="flash.events.Event")]
	[Event(name="playbackComplete", type="flash.events.Event")]
	[Event(name="scrubBegin", type="flash.events.Event")]
	[Event(name="scrub", type="flash.events.Event")]
	[Event(name="scrubEnd", type="flash.events.Event")]
	
	
	/**
	 * SimpleMP3
	 * 
	 * <p>Hopes to be the simplest way to build an MP3 player component in a Flex application.</p>
	 */
	public class SimpleMP3 extends EventDispatcher implements ISimpleMP3
	{
		// accessible
		protected var _url :String;			// url to the mp3 file.  set with load(String) or url accessor.
		protected var _autoLoad :Boolean;	// automatically load when url is set
		protected var _autoPlay :Boolean;	// automatically play when file is loaded
		protected var _autoRewind :Boolean;	// automatically rewind when playing is complete
		protected var _repeat :Boolean;		// replay when finished playing
		protected var _bufferTime :Number;	// buffer time
		protected var _volume :Number;		// 0 through 1
		protected var _panning :Number;		// -1 (left) to 0 (center) to 1 (right)
		
		protected var _stream :Boolean;				// stream?  (start playing while loading)
		protected var _streamingBufferTime :uint;	// milliseconds to buffer
		protected var _checkPolicyFile :Boolean;	// look for crossdomain.xml?
		
		// read-only bindable
		protected var _state :String;		// constant from SimpleMP3States
		protected var _id3 :ID3Info;
		
		protected var _isLoading :Boolean;	// true while loading.  
		protected var _isScrubbing :Boolean;// true while scrubbing. all other state booleans  are bindable
											// and dispatched when the state is changed.
		protected var _bytesTotal :uint;
		protected var _bytesLoaded :uint;
		protected var _bytesLoadedPercentage :Number;
		
		protected var _playbackLength :Number;
		protected var _playbackPosition :Number; // playhead location
		protected var _playbackPositionPercentage :Number; // playhead location as a percentage of length
		
		// internal
		protected var sound :Sound;
		protected var channel :SoundChannel;
		protected var playbackTimer :Timer;	// for reporting playback
		
		// event types
		public static const LOADING_BEGIN :String = "loadingBegin";
		public static const LOADING_ID3_COMPLETE :String = "loadingId3Complete";
		public static const LOADING_COMPLETE :String = "loadingComplete";
		public static const PLAYBACK_BEGIN :String = "playbackBegin";
		public static const PLAYBACK_PLAY :String = "playbackPlay";
		public static const PLAYBACK_PAUSE :String = "playbackPause";
		public static const PLAYBACK_RESUME :String = "playbackResume";
		public static const PLAYBACK_STOP :String = "playbackStop";
		public static const PLAYBACK_COMPLETE :String = "playbackComplete";
		public static const SCRUB_BEGIN :String = "scrubBegin";
		public static const SCRUB :String = "scrub";
		public static const SCRUB_END :String = "scrubEnd";
		
		
		//--------------------
		// CONSTRUCTOR
		//--------------------
		
		/**
		 * Constructor
		 */
		public function SimpleMP3(target:IEventDispatcher=null) {
			super(target);
			
			// setup
			autoLoad = true;	// auto load when the url is set
			autoPlay = true;	// auto play.  if stream, play during load. else play on load complete.
			autoRewind = false;	// auto rewind on playback complete
			repeat = false;		
			volume = 1;
			panning = 0;
			stream = false;
			streamingBufferTime = 2000;
			checkPolicyFile = false;
			playbackTimer = new Timer( 100 ); // playback data gets refreshed on this timer
			destroySound(); // initializes playback data
		}
		
		
		//--------------------
		// ACCESSIBLE
		//--------------------
		
		[Bindable]
		public function set url(value:String) :void {
			_url = value;
			if ( autoLoad ) load();
		}
		public function get url():String {
			return _url;
		}
		
		[Bindable]
		public function set autoLoad(value:Boolean) :void {
			_autoLoad = value;
			if ( autoLoad && url != null && state == SimpleMP3States.IDLE )
				load();
		}
		public function get autoLoad():Boolean {
			return _autoLoad;
		}
		
		[Bindable]
		public function set autoPlay(value:Boolean) :void {
			_autoPlay = value;
		}
		public function get autoPlay():Boolean {
			return _autoPlay;
		}
		
		[Bindable]
		public function set autoRewind(value:Boolean) :void {
			_autoRewind = value;
		}
		public function get autoRewind():Boolean {
			return _autoRewind;
		}
		
		[Bindable]
		public function set repeat(value:Boolean) :void {
			_repeat = value;
		}
		public function get repeat() :Boolean {
			return _repeat;
		}
		
		[Bindable]
		public function set volume(value:Number) :void {
			_volume = Math.min(Math.max(0, value), 1);
			if ( channel )
				channel.soundTransform = new SoundTransform( volume, panning );
		}
		public function get volume() :Number {
			return _volume;
		}
		
		[Bindable]
		public function set panning(value:Number) :void {
			_panning = Math.min(Math.max(-1, value), 1);
			if ( channel )
				channel.soundTransform = new SoundTransform( volume, panning );
		}
		public function get panning() :Number {
			return _panning;
		}
		
		[Bindable]
		public function set stream( value:Boolean ) :void {
			_stream = value;
		}
		public function get stream() :Boolean {
			return _stream;
		}
		
		[Bindable]
		public function set streamingBufferTime( milliseconds:uint ) :void {
			_streamingBufferTime = Math.min(500, milliseconds);
		}
		public function get streamingBufferTime() :uint {
			return _streamingBufferTime;
		}
		
		[Bindable]
		public function set checkPolicyFile( value:Boolean ) :void {
			_checkPolicyFile = value;
		}
		public function get checkPolicyFile() :Boolean {
			return _checkPolicyFile;
		}
				
		
		//--------------------
		// READ-ONLY BINDABLE :: STATE & STATE BOOLEAN
		//--------------------
		
		/**
		 * Return state
		 * 
		 * <p>Integrage with your mp3 player component by binding the <code>currentState</code> property
		 * to this bindable getter.</p>
		 */
		[Bindable(event="stateChanged")]
		public function get state() :String {
			return _state;
		}
		protected function setState( value:String ) :void {
			if ( value == state ) return;
			_state = value;
			dispatchEvent( new Event("stateChanged") );
		}
		
		/**
		 * isIdle? true for states: idle
		 */
		[Bindable(event="stateChanged")]
		public function get isIdle() :Boolean {
			return ( state == SimpleMP3States.IDLE );
		}
		
		/**
		 * isLoading? true for states: loading, streaming, buffering
		 */
		[Bindable(event="isLoadingChanged")]
		public function get isLoading() :Boolean {
			return _isLoading;
		}
		protected function setIsLoading( value:Boolean ) :void {
			_isLoading = value;
			dispatchEvent( new Event("isLoadingChanged") );
		}
		
		/**
		 * isPlaying? true for states: playing, streaming, buffering
		 */
		[Bindable(event="stateChanged")]
		public function get isPlaying() :Boolean {
			return ( state==SimpleMP3States.PLAYING || state==SimpleMP3States.STREAMING || state==SimpleMP3States.BUFFERING );
		}
		
		/**
		 * isStreaming? true for states: streaming, buffering
		 */
		[Bindable(event="stateChanged")]
		public function get isStreaming() :Boolean {
			return ( state==SimpleMP3States.STREAMING || state==SimpleMP3States.BUFFERING );
		}
		
		/**
		 * isBuffering? true for states: buffering
		 */
		[Bindable(event="stateChanged")]
		public function get isBuffering() :Boolean {
			return ( state==SimpleMP3States.BUFFERING );
		}
		
		/**
		 * isPaused? true for states: paused
		 */
		[Bindable(event="stateChanged")]
		public function get isPaused() :Boolean {
			return ( state==SimpleMP3States.PAUSED );
		}
		
		/**
		 * isStopped? true for states: stopped
		 */
		[Bindable(event="stateChanged")]
		public function get isStopped() :Boolean {
			return ( state==SimpleMP3States.STOPPED );
		}
		
		/**
		 * isScrubbing? true for states: scrubbing
		 */
		[Bindable(event="isScrubbingChanged")]
		public function get isScrubbing() :Boolean {
			return ( _isScrubbing );
		}
		protected function setIsScrubbing( value:Boolean ) :void {
			_isScrubbing = value;
			dispatchEvent( new Event( "isScrubbingChanged" ) );
		}
		
		
		//--------------------
		// READ-ONLY BINDABLE :: PROGRESS (BYTES & PLAYBACK)
		//--------------------
		
		/**
		 * Return bytes total
		 */
		[Bindable(event="bytesTotalChanged")]
		public function get bytesTotal() :uint {
			return _bytesTotal;
		}
		protected function setBytesTotal( value:uint ) :void {
			_bytesTotal = value;
			dispatchEvent( new Event("bytesTotalChanged") );
		}
		
		/**
		 * Return bytes loaded
		 */
		[Bindable(event="bytesLoadedChanged")]
		public function get bytesLoaded() :uint {
			return _bytesLoaded;
		}
		protected function setBytesLoaded( value:uint ) :void {
			_bytesLoaded = value;
			dispatchEvent( new Event("bytesLoadedChanged") );
		}
		
		/**
		 * Return percent loaded
		 */
		[Bindable(event="bytesLoadedPercentageChanged")]
		public function get bytesLoadedPercentage() :Number {
			return _bytesLoadedPercentage;
		}
		protected function setBytesLoadedPercentage( value:Number ) :void {
			_bytesLoadedPercentage = value;
			dispatchEvent( new Event("bytesLoadedPercentageChanged") );
		}
		 
		/**
		 * Return playback length
		 */
		[Bindable(event="playbackLengthChanged")]
		public function get playbackLength() :Number {
			return _playbackLength;
		}
		protected function setPlaybackLength( value:Number ) :void {
			_playbackLength = value;
			dispatchEvent( new Event("playbackLengthChanged") );
		}
		
		/**
		 * Return playback position
		 */
		[Bindable(event="playbackPositionChanged")]
		public function get playbackPosition() :Number {
			return _playbackPosition;
		}
		protected function setPlaybackPosition( value:Number ) :void {
			_playbackPosition = value;
			dispatchEvent( new Event("playbackPositionChanged") );
		}
		
		/**
		 * Return playback position percentage
		 */
		[Bindable(event="playbackPositionPercentageChanged")]
		public function get playbackPositionPercentage() :Number {
			return _playbackPositionPercentage;
		}
		protected function setPlaybackPositionPercentage( value:Number ) :void {
			_playbackPositionPercentage = value;
			dispatchEvent( new Event("playbackPositionPercentageChanged") );
		}
		
		/**
		 * Return ID3 info
		 */
		[Bindable(event="id3Changed")]
		public function get id3() :ID3Info {
			return _id3;
		}
		protected function setId3( value:ID3Info ) :void {
			_id3 = value != null ? value : new ID3Info();
			dispatchEvent( new Event("id3Changed") );
		}
		
		
		//--------------------
		// BASIC CONTROLS
		//--------------------
		
		/**
		 * Load
		 */
		public function load( location:String=null ) :Boolean
		{
			if ( sound ) 
				destroySound();
			url = location != null ? location : url;
			if ( !url ) // false: no url
				return false;
			else {
				sound = new Sound();
				// sound listeners
				sound.addEventListener( Event.OPEN, loadingOpenHandler, false,0,true );
				sound.addEventListener( ProgressEvent.PROGRESS, progressHandler, false,0,true );
				sound.addEventListener( Event.COMPLETE, loadingCompleteHandler, false,0,true );
				sound.addEventListener( Event.ID3, id3Handler, false,0,true );
				sound.addEventListener( IOErrorEvent.IO_ERROR, soundErrorHandler, false,0,true );
				sound.addEventListener( SecurityErrorEvent.SECURITY_ERROR, soundErrorHandler, false,0,true );
				// load
				var context :SoundLoaderContext = stream ? new SoundLoaderContext(streamingBufferTime, checkPolicyFile) : null;
				sound.load( new URLRequest(url), context );
				return true;
			}
		}
		
		/**
		 * Play
		 */
		public function play() :void
		{
			if ( !sound ) { // attempt to play() prior to load()
				load(); // make sure autoPlay is true to automatically play after loading
				return;
			}
			if ( playbackPosition == playbackLength ) setPlaybackPosition( 0 ); // rewind if play() at end
			destroyChannel();
			channel = sound.play( playbackPosition, 0, new SoundTransform(volume, panning) );
			channel.addEventListener( Event.SOUND_COMPLETE, soundCompleteHandler );
			// set state
			var streaming :Boolean = bytesLoaded < bytesTotal;
			setState( streaming ? SimpleMP3States.STREAMING : SimpleMP3States.PLAYING );
			// dispatch
			dispatchEvent( new Event( PLAYBACK_PLAY ) );
			if ( playbackPosition == 0 ) dispatchEvent( new Event( PLAYBACK_BEGIN ) );
			if ( playbackPosition > 0 && playbackPosition < playbackLength ) dispatchEvent( new Event( PLAYBACK_RESUME ) );
			// report
			startReportingPlayback();
		}
		
		/**
		 * Pause
		 */
		public function pause() :void
		{
			if ( !channel ) return; // nothing to pause
			if ( isPaused || isStopped ) return;
			setPlaybackPosition( channel.position );
			channel.stop();
			// set state
			setState( SimpleMP3States.PAUSED );
			// dispatch
			dispatchEvent( new Event( PLAYBACK_PAUSE ) );
			// stop reporting
			stopReportingPlayback();
		}
		
		/**
		 * Stop
		 */
		public function stop() :void
		{
			if ( !sound ) return; // nothing to stop
			if ( isPlaying || isPaused ) {
				destroyChannel();
				setPlaybackPosition( 0 );
				setPlaybackPositionPercentage( 0 );
				// set state
				setState( SimpleMP3States.STOPPED );
				// dispatch
				dispatchEvent( new Event( PLAYBACK_STOP ) );
				// stop reporting
				stopReportingPlayback();
			}
		}
		
		/**
		 * Scrub begin
		 */
		public function scrubBegin() :void
		{
			if ( !sound ) return; // nothing to scrub
			stopReportingPlayback();
			destroyChannel();
			setIsScrubbing( true );
			// dispatch
			dispatchEvent( new Event( SCRUB_BEGIN ) );
		}
		
		/**
		 * Scrub
		 */
		public function scrub(position:Number) :void
		{
			// validate - can't scrub past loaded data
			var scrubTo :Number = Math.min(Math.max(0, position), sound.length);
			// no scrubBegin().  immediately scrub once as in a track click.
			if ( !isScrubbing ) {
				scrubBegin();
				scrub( scrubTo );
				scrubEnd();
			} 
			// scrubBegin() was called.  smooth scrubbing as in dragging a slider thumb.
			else {
				setPlaybackPosition( scrubTo );
				setPlaybackPositionPercentage( scrubTo > 0 ? scrubTo / playbackLength * 100 : 0 );
				// dispatch
				dispatchEvent( new Event( SCRUB ) );
			}
		}
		
		/**
		 * Scrub end
		 */
		public function scrubEnd() :void
		{
			setIsScrubbing( false );
			// dispatch
			dispatchEvent( new Event( SCRUB_END ) );
			if ( isPlaying )
				play();
			else if ( isStopped && playbackPosition > 0 ) {
				setState( SimpleMP3States.PAUSED );
				// dispatch
				dispatchEvent( new Event( PLAYBACK_PAUSE ) );
			}
		}
		
		
		//--------------------
		// PRIVATE CONTROLS
		//--------------------
		
		/**
		 * Destroy channel
		 */		
		protected function destroyChannel() :void {
			if ( channel ) {
				channel.removeEventListener( Event.SOUND_COMPLETE, soundCompleteHandler );
				try {
					channel.soundTransform = new SoundTransform(0); // if streaming, at least kill the volume
					channel.stop();
				} catch ( error:Error ) {}
				channel = null; // gc eligible
			}
		}
		
		/**
		 * Destroy sound and reset.  Also destroys channel.
		 */	
		protected function destroySound() :void {
			stopReportingPlayback();
			if ( sound ) {
				// remove listeners
				sound.removeEventListener( Event.OPEN, loadingOpenHandler );
				sound.removeEventListener( Event.COMPLETE, loadingCompleteHandler );
				sound.removeEventListener( ProgressEvent.PROGRESS, progressHandler );
				sound.removeEventListener( Event.ID3, id3Handler );
				sound.removeEventListener( IOErrorEvent.IO_ERROR, soundErrorHandler );
				sound.removeEventListener( SecurityErrorEvent.SECURITY_ERROR, soundErrorHandler );
				try {
					sound.close();
				} catch ( error:Error ) {}
				sound = null; // gc eligible
			}
			// set state
			setState( SimpleMP3States.IDLE );
			// reset
			setIsLoading( false );
			setBytesLoadedPercentage( 0 );
			setBytesLoaded( 0 );
			setBytesTotal( 0 );
			setPlaybackPositionPercentage( 0 );
			setPlaybackPosition( 0 );
			setPlaybackLength( 0 );
			setId3( null );
			// destroy channel
			destroyChannel();
		}
		
		/**
		 * Start playback events
		 */	
		protected function startReportingPlayback() :void {
			if ( !playbackTimer.hasEventListener( TimerEvent.TIMER ) ) { 
				playbackTimer.addEventListener( TimerEvent.TIMER, reportPlayback, false,0,true );
			}
			playbackTimer.start();
		}
		
		/**
		 * Stop playback events
		 */	
		protected function stopReportingPlayback() :void {
			if ( playbackTimer.hasEventListener(TimerEvent.TIMER) ) { 
				playbackTimer.removeEventListener( TimerEvent.TIMER, reportPlayback );
			}
			playbackTimer.stop();
		}
		
		
		//--------------------
		// INTERNAL EVENT HANDLERS / DISPATCHERS
		//--------------------
		
		/**
		 * Loading Begin
		 */
		protected function loadingOpenHandler ( event:Event ) :void {
			// set state
			setState( SimpleMP3States.LOADING );
			setIsLoading( true );
			// dispatch
			dispatchEvent( new Event( LOADING_BEGIN ) );
			// autoPlay w/ stream?  play.
			if ( autoPlay && stream ) play(); 
		}
		
		/**
		 * Loading Progress
		 */
		protected function progressHandler ( event:ProgressEvent ) :void {
			if ( bytesTotal != event.bytesTotal ) {
				// bytes total
				setBytesTotal( event.bytesTotal ); 
			}
			if ( bytesLoaded != event.bytesLoaded ) { 
				// bytes loaded
				setBytesLoaded( event.bytesLoaded );
				// bytes loaded percentage
				setBytesLoadedPercentage( bytesTotal > 0 ? bytesLoaded / bytesTotal * 100 : 0 );
				// set estimated length, except when scrubbing.  setting the estimated length while scrubbing can be tricky to handle in a ui.
				if ( !isScrubbing )
				setPlaybackLength( bytesTotal > 0 ? sound.length / (bytesLoaded / bytesTotal) : 0 );
			}
			// dispatch loading progress event
			dispatchEvent( new SimpleMP3ProgressEvent( SimpleMP3ProgressEvent.LOADING_PROGRESS, bytesLoaded, bytesTotal ) );
		}
		
		/**
		 * Loading Complete
		 */
		protected function loadingCompleteHandler ( event:Event ) :void {
			setPlaybackLength( sound.length ); // set actual length
			setIsLoading( false );
			// dispatch
			dispatchEvent( new Event( LOADING_COMPLETE ) );
			if ( isScrubbing ) 
				return;
			if ( !stream && autoPlay ) 
				play();
			else if ( !stream && !autoPlay ) {
				setState( SimpleMP3States.STOPPED ); // else do nothing
				// dispatch
				dispatchEvent( new Event( PLAYBACK_STOP ) );
			}
		}
		
		/**
		 * Dispatch playback events
		 */	
		protected function reportPlayback ( event:TimerEvent ) :void {
			// position
			setPlaybackPosition( playbackLength > 0 ? channel.position : 0 );
			// position percentage
			setPlaybackPositionPercentage( playbackLength > 0 ? channel.position / playbackLength * 100 : 0 );
			// buffering, streaming, playing?
			if ( bytesLoaded < bytesTotal && sound.isBuffering ) // on subsequent playbacks, sound.isBuffering may return a false positive
				setState( SimpleMP3States.BUFFERING );
			else 
				setState( bytesLoaded < bytesTotal ? SimpleMP3States.STREAMING : SimpleMP3States.PLAYING );
			// dispatch playback progress event
			dispatchEvent( new SimpleMP3ProgressEvent( SimpleMP3ProgressEvent.PLAYBACK_PROGRESS, playbackPosition, playbackLength ) );
		}
		
		/**
		 * Sound Complete
		 */
		protected function soundCompleteHandler ( event:Event ) :void {
			stopReportingPlayback();
			setPlaybackPosition( playbackLength );	// nudge
			setPlaybackPositionPercentage( 100 );	// nudge
			// dispatch playback progress event
			dispatchEvent( new SimpleMP3ProgressEvent( SimpleMP3ProgressEvent.PLAYBACK_PROGRESS, playbackPosition, playbackLength ) );
			// dispatch playback complete event
			dispatchEvent( new Event( PLAYBACK_COMPLETE ) );
			if ( repeat )
				play();
			else if ( autoRewind )
				stop();
			else {
				setState( SimpleMP3States.PAUSED );
				// dispatch
				dispatchEvent( new Event( PLAYBACK_PAUSE ) );
			}
		}
		
		/**
		 * ID3 Handler
		 */
		protected function id3Handler ( event:Event ) :void {
			if ( sound.id3 ) {
				setId3( sound.id3 );
				// dispatch
				dispatchEvent( new Event( LOADING_ID3_COMPLETE ) );
			}
		}
		
		/**
		 * Sound error handler
		 */
		protected function soundErrorHandler ( event:Event ) :void {
			// handle errors
			trace( event.type );
		}
		
		
		//--------------------
		// CLASS METHODS
		//--------------------
		
		/**
		 * Milliseconds to time string
		 * 
		 * <p>Return a string formatted [HH:]MM:SS for the length in milliseconds</p>
		 */
		public static function msToTimeString( milliseconds:Number ) :String {
			var totalSeconds :Number = milliseconds/1000;
			var h :uint = Math.floor( totalSeconds / 3600 );
			var hours :String = h<10 ? "0"+h.toString() : h.toString();
			var m :uint = Math.floor( totalSeconds / 60 );
			var minutes :String = m<10 ? "0"+m.toString() : m.toString();
			var s :uint = totalSeconds % 60;
			var seconds :String = s<10 ? "0"+s.toString() : s.toString();
			return h>0?hours+":":"" + minutes + ":" + seconds;
		}
		
	}
}